const isObject = (val) => val && typeof val === "object";
const sortObject = require("./utils/sortObject");
const writeFileTree = require("./utils/writeFileTree");
const normalizeFilePaths = require("./utils/normalizeFilePaths");
const fs = require("fs-extra");
const path = require("path");
const slash = require("slash");
const globby = require("globby");
const ejs = require("ejs");
const { isBinaryFileSync } = require("isbinaryfile");
const ConfigTransform = require("./ConfigTransform");

const defaultConfigTransforms = {
  babel: new ConfigTransform({
    file: {
      js: ["babel.config.js"],
    },
  }),
  postcss: new ConfigTransform({
    file: {
      js: ["postcss.config.js"],
      json: [".postcssrc.json", ".postcssrc"],
      yaml: [".postcssrc.yaml", ".postcssrc.yml"],
    },
  }),
  eslintConfig: new ConfigTransform({
    file: {
      js: [".eslintrc.js"],
      json: [".eslintrc", ".eslintrc.json"],
      yaml: [".eslintrc.yaml", ".eslintrc.yml"],
    },
  }),
  jest: new ConfigTransform({
    file: {
      js: ["jest.config.js"],
    },
  }),
  browserslist: new ConfigTransform({
    file: {
      lines: [".browserslistrc"],
    },
  }),
};

const reservedConfigTransforms = {
  vue: new ConfigTransform({
    file: {
      js: ["vue.config.js"],
    },
  }),
};

const ensureEOL = (str) => {
  if (str.charAt(str.length - 1) !== "\n") {
    return str + "\n";
  }

  return str;
};

class Generator {
  constructor(pkg, context) {
    // package.json信息
    this.pkg = pkg;
    this.context = context;
    this.files = {};
    this.fileMiddlewares = [];
    this.options = {};
    this.rootOptions = {};
  }

  // 扩展package.json
  extendPackage(fields) {
    const pkg = this.pkg;
    Object.keys(fields).forEach((key) => {
      const newValue = fields[key];
      const oldValue = pkg[key];

      if (
        isObject(newValue) &&
        (key === "dependencies" ||
          key === "scripts" ||
          key === "devDependencies")
      ) {
        pkg[key] = { ...oldValue, ...newValue };
      } else {
        pkg[key] = newValue;
      }
    });
  }

  // 使用 ejs 解析 lib\generator\xx\template 中的文件
  async resolveFiles() {
    const files = this.files;
    for (const middleware of this.fileMiddlewares) {
      await middleware(files, ejs.render);
    }
    // 反斜杠\ 转换成正斜杠 /
    normalizeFilePaths(files);
  }

  render(filePath, options = {}, ejsOptions = {}) {
    const baseDir = extractCallDir();
    filePath = path.resolve(baseDir, filePath);
    this._injectFileMiddleware(async (files) => {
      const data = this._resolveData(options);

      // globby 读取template/下所有文件
      let _files = await globby(["**/*"], { cwd: filePath, dot: true });
      // 选择了typescript要做处理
      if (data.needJudgeTsOrJs === true) {
        if (data.useTypescript === true) {
          _files = _files.filter((name) => {
            const parsed = path.parse(name);
            console.log(parsed);
            return parsed.ext !== ".js";
          });
        } else {
          _files = _files.filter((name) => {
            const parsed = path.parse(name);
            return parsed.ext !== ".ts";
          });
        }
      }
      for (const rawPath of _files) {
        const truePath = path.resolve(filePath, rawPath);

        const content = this.renderFile(truePath, data, ejsOptions);

        // only set file if it's not all whitespace, or is a Buffer (binary files)
        if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
          files[rawPath] = content;
        }
      }
    });
  }

  async generate() {
    // 从 package.json 中提取文件
    this.extractConfigFiles();
    // 解析templaten内文件内容
    await this.resolveFiles();

    this.sortPkg();
    this.files["package.json"] = JSON.stringify(this.pkg, null, 2) + "\n";
    // 将文件写入创建的目录
    writeFileTree(this.context, this.files);
  }

  renderFile(filename, data, ejsOptions) {
    // 二进制文件直接返回
    if (isBinaryFileSync(filename)) {
      return fs.readFileSync(filename);
    }

    const template = fs.readFileSync(filename, "utf-8");
    return ejs.render(template, data, ejsOptions);
  }

  // 按照下面的顺序对 package.json 中的 key 进行排序
  sortPkg() {
    // ensure package.json keys has readable order
    this.pkg.dependencies = sortObject(this.pkg.dependencies);
    this.pkg.devDependencies = sortObject(this.pkg.devDependencies);
    this.pkg.scripts = sortObject(this.pkg.scripts, [
      "dev",
      "build",
      "test:unit",
      "test:e2e",
      "lint",
      "deploy",
    ]);

    this.pkg = sortObject(this.pkg, [
      "name",
      "version",
      "private",
      "description",
      "author",
      "scripts",
      "husky",
      "lint-staged",
      "main",
      "module",
      "browser",
      "jsDelivr",
      "unpkg",
      "files",
      "dependencies",
      "devDependencies",
      "peerDependencies",
      "vue",
      "babel",
      "eslintConfig",
      "prettier",
      "postcss",
      "browserslist",
      "jest",
    ]);
  }

  /**
   * Add import statements to a file.
   */
  injectImports(file, imports) {
    const _imports = this.imports[file] || (this.imports[file] = new Set());
    (Array.isArray(imports) ? imports : [imports]).forEach((imp) => {
      _imports.add(imp);
    });
  }

  /**
   * Add options to the root Vue instance (detected by `new Vue`).
   */
  injectRootOptions(file, options) {
    const _options =
      this.rootOptions[file] || (this.rootOptions[file] = new Set());
    (Array.isArray(options) ? options : [options]).forEach((opt) => {
      _options.add(opt);
    });
  }

  // 将 package.json 中的配置提取出来，生成单独的文件
  // 例如将 package.json 中的
  // babel: {
  //     presets: ['@babel/preset-env']
  // },
  // 提取出来变成 babel.config.js 文件
  extractConfigFiles() {
    const configTransforms = {
      ...defaultConfigTransforms,
      ...this.configTransforms,
      ...reservedConfigTransforms,
    };

    const extract = (key) => {
      if (configTransforms[key] && this.pkg[key]) {
        const value = this.pkg[key];
        const configTransform = configTransforms[key];
        const res = configTransform.transform(value, this.files, this.context);

        const { content, filename } = res;
        // 如果文件不是以 \n 结尾，则补上 \n
        this.files[filename] = ensureEOL(content);
        delete this.pkg[key];
      }
    };

    extract("vue");
    extract("babel");
  }

  _injectFileMiddleware(middleware) {
    this.fileMiddlewares.push(middleware);
  }

  // 合并选项
  _resolveData(additionalData) {
    return {
      options: this.options,
      rootOptions: this.rootOptions,
      ...additionalData,
    };
  }
}

// http://blog.shaochuancs.com/about-error-capturestacktrace/
// 获取调用栈信息
function extractCallDir() {
  const obj = {};
  Error.captureStackTrace(obj);
  // 在 lib\generator\xx 等各个模块中 调用 generator.render()
  // 将会排在调用栈中的第四个，也就是 obj.stack.split('\n')[3]
  const callSite = obj.stack.split("\n")[3];

  // the regexp for the stack when called inside a named function
  const namedStackRegExp = /\s\((.*):\d+:\d+\)$/;
  // the regexp for the stack when called inside an anonymous
  const anonymousStackRegExp = /at (.*):\d+:\d+$/;

  let matchResult = callSite.match(namedStackRegExp);
  if (!matchResult) {
    matchResult = callSite.match(anonymousStackRegExp);
  }

  const fileName = matchResult[1];
  // 获取对应文件的目录
  return path.dirname(fileName);
}

module.exports = Generator;
